package main;

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Random;

import util.GeradorAmostra;
import util.Util;
import util.estatisticas.AnalisadorFaseTransiente;
import util.estatisticas.AnalisadorNumeroUsuarios;
import util.estatisticas.AnalisadorTempoDownload;
import util.estatisticas.Estatistica;
import util.exceptions.AnalisadorFaseTransienteInvalidoException;
import util.exceptions.NaoExisteBlocoASerTrocadoException;
import util.exceptions.NaoExistemPeersSuficientesParaSeFazerTroca;
import util.exceptions.PoliticaDeSelecaoInvalidaException;
import util.exceptions.TaxaZeroException;
import util.output.Debug;
import util.output.Saida;
import entidades.Evento;
import entidades.Peer;
import entidades.Publisher;
import entidades.Seed;
import entidades.evento.EventoChegadaExterna;
import entidades.evento.EventoSaidaSistema;
import entidades.evento.EventoTrocaConteudoPeerPeer;
import entidades.evento.EventoTrocaConteudoPeerPublisher;
import entidades.evento.EventoTrocaConteudoPeerSeed;
import entidades.politica.PoliticaSelecaoBlocos;
import entidades.politica.PoliticaSelecaoBlocosMaisRaro;
import entidades.politica.PoliticaSelecaoBlocosRandomica;
import entidades.politica.PoliticaSelecaoVizinhos;
import entidades.politica.PoliticaSelecaoVizinhosPorGrupos;
import entidades.politica.PoliticaSelecaoVizinhosRandomica;


public class Main {
	
	private static final int RODADA_TRANSIENTE = 0;
	private static final int NUMERO_DE_RODADAS = 30;
	
	private static Publisher publisher;
	private static PriorityQueue<Evento> listaEventos = new PriorityQueue<Evento>();
	private static ArrayList<Peer> listaPeers = new ArrayList<Peer>();
	private static ArrayList<Seed> listaSeeds = new ArrayList<Seed>();

	private static int chegadasRodadaAtual = 0;
	private static int rodada = RODADA_TRANSIENTE;
	private static Estatistica estatistica;
	private static AnalisadorFaseTransiente analisadorFaseTransiente;
	private static AnalisadorNumeroUsuarios analisadorNumeroUsuarios = new AnalisadorNumeroUsuarios();
	private static AnalisadorTempoDownload analisadorTempoDownload = new AnalisadorTempoDownload();
	
	private static PoliticaSelecaoVizinhos politicaSelecaoVizinhos;
	private static PoliticaSelecaoBlocos politicaSelecaoBlocos;

	private static double tempo = 0;
	private static boolean fimSimulacao = false;
	
	public static void main(String[] args) throws PoliticaDeSelecaoInvalidaException, AnalisadorFaseTransienteInvalidoException {
		
		configuraSimulacao();
		
		estatistica = new Estatistica(RODADA_TRANSIENTE, Util.N0);
		
		// criando o publisher
		publisher = new Publisher(politicaSelecaoVizinhos, politicaSelecaoBlocos);
		
		// inicializando a lista de peers e criando eventos de troca entre peers
		for (int i = 0; i < Util.N0; i++) { 
			criaPeer(false);
		}
		
		// cria assinatura diferenciada para os peers, caso tenha sido configurado para isso.
		for (int i = 0; i < listaPeers.size(); i++) {
			Peer peer = listaPeers.get(i);
			peer.setAssinatura(Util.criarAssinaturaDiferenciada());
		}
		
		// criando evento de troca entre seeds e peers
		for (int i = 0; i < listaSeeds.size(); i++) {
			criaEventoTrocaConteudoPeerSeed(listaSeeds.get(i));
		}
		
		// criando evento de troca entre publisher e peers
		criaEventoTrocaConteudoPeerPublisher();

		// criando evento de chegada externa;
		criaEventoChegadaExterna();
		
		long start = System.currentTimeMillis();
		simulacao();
		long end = System.currentTimeMillis();
		
		Debug.imprime("=*= Configuração da simulação =*=");
		Debug.imprime("Seed gerador de amostras exponenciais: " + GeradorAmostra.SEED);
		Debug.imprime("Métrica para cálculo do fim da fase transiente: " + Util.METRICA);
		Debug.imprime("Número de usuários iniciais: " + Util.N0);
		Debug.imprime("Política de seleção de blocos: " + Util.POLITICA_SELECAO_BLOCOS);
		Debug.imprime("Política de seleção de vizinhos: " + Util.POLITICA_SELECAO_VIZINHOS);
		Debug.imprime("\n");
		Debug.imprime("=*= Simulação =*=");
		Debug.imprime("Duração: " + (end-start));
		Debug.imprime("Número de rodadas: " + rodada);
		Debug.imprime("Mais informações: ver gráficos");
	}
	
	/**
	 * Configura alguns parâmetros para execução da simulação.
	 * 
	 * @throws PoliticaDeSelecaoInvalidaException 
	 * @throws AnalisadorFaseTransienteInvalidoException 
	 */
	private static void configuraSimulacao() throws PoliticaDeSelecaoInvalidaException, AnalisadorFaseTransienteInvalidoException {
		if (!Util.NOME_ARQUIVO.isEmpty()) {
			Saida.noArquivo(Util.NOME_ARQUIVO);
			Saida.ativar();
		}
		else {
			Saida.desativar();
		}
		
		if (!Util.NOME_ARQUIVO_DEBUG.isEmpty()) {
			Debug.noArquivo(Util.NOME_ARQUIVO_DEBUG);
			Debug.ativarArquivo();
		}
		else {
			Debug.desativarArquivo();
		}
		
		if (Util.DEBUG_CONSOLE) {
			Debug.ativarConsole();
		}
		else {
			Debug.desativarConsole();
		}
		
		// definindo a política de seleção de vizinhos
		if (Util.POLITICA_SELECAO_VIZINHOS.equals(Util.POLITICA_SELECAO_VIZINHOS_RANDOMICA)) {
			politicaSelecaoVizinhos = new PoliticaSelecaoVizinhosRandomica(listaPeers);
		}
		else if (Util.POLITICA_SELECAO_VIZINHOS.equals(Util.POLITICA_SELECAO_VIZINHOS_POR_GRUPOS)) {
			politicaSelecaoVizinhos = new PoliticaSelecaoVizinhosPorGrupos(listaPeers);
		}
		else {
			throw new PoliticaDeSelecaoInvalidaException();
		}
		
		// definindo a política de seleção de blocos
		if (Util.POLITICA_SELECAO_BLOCOS.equals(Util.POLITICA_SELECAO_BLOCOS_RANDOMICA_UTIL)) {
			politicaSelecaoBlocos = new PoliticaSelecaoBlocosRandomica();
		}
		else if (Util.POLITICA_SELECAO_BLOCOS.equals(Util.POLITICA_SELECAO_BLOCOS_MAIS_RARO_UTIL)) {
			politicaSelecaoBlocos = new PoliticaSelecaoBlocosMaisRaro();
		}
		else {
			throw new PoliticaDeSelecaoInvalidaException();
		}
		
		// definindo analisador da fase transiente 
		if (Util.METRICA.equals(Util.METRICA_FASE_TRANSIENTE_NUMERO_USUARIOS)) {
			analisadorFaseTransiente = analisadorNumeroUsuarios;
		}
		else if (Util.METRICA.equals(Util.METRICA_FASE_TRANSIENTE_TEMPO_DOWNLOAD)) {
			analisadorFaseTransiente = analisadorTempoDownload;
		}
		else {
			throw new AnalisadorFaseTransienteInvalidoException();
		}
	}
	
	/**
	 * Simulação.
	 * 
	 */
	private static void simulacao() {
		Debug.imprimeConsole("Início da simulação");
		while (!fimSimulacao ) {

			Evento evento = listaEventos.poll();
			tempo = evento.getTempo();
			
			// tratamento dos eventos
			if (evento instanceof EventoChegadaExterna) {
				tratamentoChegadaExterna();
			}
			else if (evento instanceof EventoSaidaSistema) {
				tratamentoSaidaSistema((EventoSaidaSistema) evento);
			}
			else if (evento instanceof EventoTrocaConteudoPeerPublisher) {
				tratamentoTrocaConteudoPeerPublisher();
			}
			else if (evento instanceof EventoTrocaConteudoPeerSeed) {
				tratamentoTrocaConteudoPeerSeed((EventoTrocaConteudoPeerSeed) evento);
			}
			else if (evento instanceof EventoTrocaConteudoPeerPeer) {
				tratamentoTrocaConteudoPeerPeer((EventoTrocaConteudoPeerPeer) evento);
			}
		}
		
		Debug.imprimeConsole("Finalizando Simulação.. Iniciando plot..");
		
		analisadorFaseTransiente.plotPontoEquilibrio();
		analisadorNumeroUsuarios.plot();
		analisadorTempoDownload.plot();
		estatistica.finalizaSimulacao();
		
		Debug.imprimeConsole("Fim Simulação");
	}
	
	/**
	 * Verifica se o sistema entrou em equilibrio
	 * 
	 */
	private static void verificaSistemaEmEquilibrio() {
		if (analisadorFaseTransiente.isEquilibrio() && rodada == RODADA_TRANSIENTE) {
			rodada = RODADA_TRANSIENTE + 1;
			chegadasRodadaAtual = 0;
			estatistica.inicializaRodada(rodada, tempo, listaPeers.size() + listaSeeds.size());
		}
	}
	
	/**
	 * Atualiza as chegadas de peer na rodada e, caso seja necessário, cria uma nova rodada.
	 * 
	 */
	private static void atualizaChegadasNaRodada() {
		if (analisadorFaseTransiente.isEquilibrio()) {

			if (chegadasRodadaAtual == Util.COLETAS_POR_RODADA) {
				Debug.imprimeConsole("Trocou rodada. Rodada atual: " + rodada);
				finalizaRodada();
			}
			
			chegadasRodadaAtual++;
		}
	}

	/**
	 * Finaliza uma rodada e inicia outra, caso seja necessário.
	 * 
	 */
	private static void finalizaRodada() {
		estatistica.finalizaRodada(tempo);
		
		if (rodada >= NUMERO_DE_RODADAS) {
			fimSimulacao = estatistica.isIntervaloConfiancaSatisfeito();
		}
		
		chegadasRodadaAtual = 0;
		rodada++;
		estatistica.inicializaRodada(rodada, tempo, listaPeers.size() + listaSeeds.size());
	}
	
	/**
	 * Instancia o peer e insere na lista. Ajusta a rodada em que se encontra.
	 * 
	 * @param recomendado
	 * @return
	 */
	private static Peer inserePeer(boolean recomendado) {
		analisadorNumeroUsuarios.chegadaUsuario(tempo);

		verificaSistemaEmEquilibrio();
		atualizaChegadasNaRodada();
		
		// O peer que "equilibou o sistema" conta para a rodada 1
		Peer peer = new Peer(tempo, politicaSelecaoVizinhos, politicaSelecaoBlocos, recomendado);
		peer.setRodada(rodada);
		listaPeers.add(peer);
		
		if (analisadorFaseTransiente.isEquilibrio()) {
			estatistica.chegadaUsuario(peer);
		}
		
		return peer;
	}
	
	/**
	 * Instancia a seed e insere na lista. Ajusta a rodada em que se encontra.
	 * 
	 * @param peer
	 * @return
	 */
	private static Seed insereSeed(Peer peer) {
		Seed seed = new Seed(tempo, politicaSelecaoVizinhos, politicaSelecaoBlocos);
		seed.setRodada(rodada);
		
		listaSeeds.add(seed);		
		analisadorTempoDownload.encerraDownload(peer.getInstanteChegada(), peer.getInstanteSaida());
		
		return seed;
	}
	
	/**
	 * Cria um peer novo no sistema.
	 * Evento de troca de bloco é agendado.
	 * 
	 * @param peer
	 */
	private static void criaPeer(boolean recomendacao) {
		Peer peer = inserePeer(recomendacao);
		
		criaEventoTrocaConteudoPeerPeer(peer);
	}
	
	/**
	 * Cria uma seed a partir do peer.
	 * Eventos de troca de bloco e saida do sistema já são agendados.
	 * 
	 * @param peer
	 */
	private static void criaSeed(Peer peer) {
		removePeer(peer);

		Seed seed = insereSeed(peer);
		
		// saida instantânea
		if (Util.GAMA < 0) {
			criaEventoSaidaSistema(seed);
		} else {
			criaEventoTrocaConteudoPeerSeed(seed);
			criaEventoSaidaSistema(seed);
		}
	}
	
	/**
	 * Remove o peer da lista e atualiza seus dados nas estatísticas.
	 * 
	 * @param peer
	 */
	private static void removePeer(Peer peer) {
		peer.setInstanteSaida(tempo);
		listaPeers.remove(peer);
	}
	
	/**
	 * Remove a seed da lista e atualiza seus dados nas estatísticas.
	 * 
	 * @param seed
	 */
	private static void removeSeed(Seed seed) {
		analisadorNumeroUsuarios.saidaUsuario(tempo);

		verificaSistemaEmEquilibrio();
		seed.setInstanteSaida(tempo);
		listaSeeds.remove(seed);
		
		if (analisadorFaseTransiente.isEquilibrio()) {
			estatistica.saidaUsuario(seed);
		}
	}
	
	/**
	 * Um novo peer entra no sistema com uma probabilidade p.
	 * Evento de troca de bloco já é agendado para o peer, caso ele entre no sistema.
	 */
	private static void fazRecomendacao() {
		Random random = new Random();
		if (random.nextDouble() <= Util.P) {
			criaPeer(true);
		}
	}
	
	/**
	 * Método de tratamento do evento de troca de conteúdo entre Peers.
	 * 
	 * @param evento
	 */
	private static void tratamentoTrocaConteudoPeerPeer(EventoTrocaConteudoPeerPeer evento) {
		Peer peerOrigem = evento.getPeer();
		
		// não realiza troca caso esse peer tenha virado seed
		// isso pode acontecer e optamos por não realizar o tratamento do evento
		// ao invés de removê-lo da lista de eventos.
		if (peerOrigem.virouSeed()) {
			return;
		}
		
		try {
			Peer peerDestino = peerOrigem.selecionaPeer();
			int bloco = peerOrigem.selecionaBloco(peerDestino);
			
			peerDestino.recebeBloco(bloco);
			
			peerOrigem.setTrocasComPeer();

			if (peerDestino.virouSeed()) {
				criaSeed(peerDestino);
			}
		}
		catch (NaoExistemPeersSuficientesParaSeFazerTroca e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
		catch (NaoExisteBlocoASerTrocadoException e) {
			peerOrigem.setTrocasComPeerMalSucedidas();
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
		finally {
			criaEventoTrocaConteudoPeerPeer(peerOrigem);
		}
		
	}
	
	/**
	 * Método para agendamento do evento de troca de conteúdo entre Peers.
	 * 
	 * @param peer
	 */
	private static void criaEventoTrocaConteudoPeerPeer(Peer peer) {
		try {
			listaEventos.add( new EventoTrocaConteudoPeerPeer(tempo, peer) );
		}
		catch (TaxaZeroException e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
	}
	
	/**
	 * Método de tratamento do evento de troca de conteúdo entre um Seed e um Peer.
	 * Evento de troca de bloco já é agendado.
	 * 
	 * @param evento
	 */
	private static void tratamentoTrocaConteudoPeerSeed(EventoTrocaConteudoPeerSeed evento) {
		Seed seed = evento.getSeed();
		
		// não realiza troca caso essa seed tenha saido do sistema
		// isso pode acontecer e optamos por não realizar o tratamento do evento
		// ao invés de removê-lo da lista de eventos.
		if (seed.saiuSistema()) {
			return;
		}
		
		try {
			Peer peerDestino = seed.selecionaPeer();
			int bloco = seed.selecionaBloco(peerDestino);
			
			peerDestino.recebeBloco(bloco);

			if (peerDestino.virouSeed()) {
				criaSeed(peerDestino);
			}
		}
		catch (NaoExistemPeersSuficientesParaSeFazerTroca e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
		finally {
			criaEventoTrocaConteudoPeerSeed(seed);
		}
	}
	
	/**
	 * Método para agendamento do evento de troca de conteúdo entre um Seed e um Peer. 
	 * 
	 * @param seed
	 */
	private static void criaEventoTrocaConteudoPeerSeed(Seed seed) {
		try {
			listaEventos.add( new EventoTrocaConteudoPeerSeed(tempo, seed) );
		}
		catch (TaxaZeroException e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}	
	}
	
	/**
	 * Método de tratamento do evento de troca de conteúdo entre o Publisher e um Peer.
	 * Evento de troca de bloco já é agendado. 
	 */
	private static void tratamentoTrocaConteudoPeerPublisher() {		
		try {
			Peer peerDestino = publisher.selecionaPeer();
			int bloco = publisher.selecionaBloco(peerDestino);
			
			peerDestino.recebeBloco(bloco);
			
			if (peerDestino.virouSeed()) {
				criaSeed(peerDestino);
			}
		}
		catch (NaoExistemPeersSuficientesParaSeFazerTroca e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
		finally {
			criaEventoTrocaConteudoPeerPublisher();
		}
	}
	
	/**
	 * Método para agendamento do evento de troca de conteúdo entre o Publisher e um Peer. 
	 */
	private static void criaEventoTrocaConteudoPeerPublisher() {
		try {
			listaEventos.add( new EventoTrocaConteudoPeerPublisher(tempo) );
		}
		catch (TaxaZeroException e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}		
	}
	
	/**
	 * Método de tratamento do evento de saida de uma Seed do sistema.
	 * 
	 * @param evento
	 */
	private static void tratamentoSaidaSistema(EventoSaidaSistema evento) {
		removeSeed(evento.getSeed());
		fazRecomendacao();
	}
	
	/**
	 * Método para agendamento do evento de saida de uma Seed do sistema.
	 * 
	 * @param seed
	 */
	private static void criaEventoSaidaSistema(Seed seed) {
		try {
			listaEventos.add( new EventoSaidaSistema(tempo, seed) );
		}
		catch (TaxaZeroException e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}
	}
	
	/**
	 * Método de tratamento do evento de chegada externa.
	 * Evento de chegada externa já é agendado.
	 */
	private static void tratamentoChegadaExterna() {
		criaPeer(false);
		criaEventoChegadaExterna();
	}
	
	/**
	 * Método para agendamento do evento de chegada externa
	 */
	private static void criaEventoChegadaExterna() {
		try {
			listaEventos.add( new EventoChegadaExterna(tempo) );
		}
		catch (TaxaZeroException e) {
			//Debug.imprime("! " + e.getClass().getSimpleName());
		}		
	}
}
